/* Copyright (C) 2007-2008  klevgert  http://code.google.com/p/db4o-netbeans/

This file is part of the db4o-netbeans module suite.

db4o-netbeans is free software; you can redistribute it and/or modify it under
the terms of version 2 of the GNU General Public License as published
by the Free Software Foundation and as clarified by db4objects' GPL 
interpretation policy, available at
http://www.db4o.com/about/company/legalpolicies/gplinterpretation/
Alternatively you can write to db4objects, Inc., 1900 S Norfolk Street,
Suite 350, San Mateo, CA 94403, USA.

db4o-netbeans is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
for more details.

You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
59 Temple Place - Suite 330, Boston, MA  02111-1307, USA. */


package com.db4o.nb.editor.nodes;

import com.db4o.ObjectContainer;
import com.db4o.ObjectSet;
import com.db4o.ext.Db4oException;
import com.db4o.nb.api.Constrain;
import com.db4o.nb.api.Db4oProvider;
import com.db4o.nb.api.Descend;
import com.db4o.nb.api.QueryData;
import com.db4o.nb.api.SodaQueryData;
import com.db4o.nb.api.exception.Db4oProviderException;
import com.db4o.nb.api.impl.Constants;
import com.db4o.nb.editor.Db4oDataEditor;
import com.db4o.nb.editor.dialogs.EnterDescendDialog;
import com.db4o.nb.editor.properties.SodaPropertyEditor;
import com.db4o.nb.util.ClassLoaderUtil;
import com.db4o.nb.util.Db4oReflectorUtil;
import com.db4o.nb.util.DialogUtil;
import com.db4o.query.Constraint;
import com.db4o.query.Query;
import com.spaceprogram.db4o.sql.ObjectSetWrapper;
import com.spaceprogram.db4o.sql.Sql4o;
import com.spaceprogram.db4o.sql.Sql4oException;
import com.spaceprogram.db4o.sql.parser.SqlParseException;
import java.awt.HeadlessException;
import java.awt.event.ActionEvent;
import java.lang.reflect.Constructor;
import java.util.Iterator;
import java.util.List;
import javax.swing.AbstractAction;
import javax.swing.Action;
import org.openide.ErrorManager;
import org.openide.actions.PropertiesAction;
import org.openide.loaders.DataObject;
import org.openide.nodes.AbstractNode;
import org.openide.nodes.Children;
import org.openide.nodes.Node;
import org.openide.nodes.Node.Property;
import org.openide.nodes.PropertySupport;
import org.openide.nodes.Sheet;
import org.openide.util.NbBundle;
import org.openide.util.actions.SystemAction;
import org.openide.util.lookup.Lookups;
import org.w3c.dom.DOMException;

/**
 * Node that represents a db4o database query.
 *
 * @author klevgert
 */
public class QueryNode extends AbstractNode {
  
  private static final String IMAGE_ICON_BASE = "com/db4o/nb/editor/resources/query.gif";
  
  /** Holds query related api data.*/
  private QueryData queryData;
  
  /** Creates a new instance of QueryNode */
  public QueryNode(QueryData queryData, DataObject dataObj) {
    super( (queryData.getKind().equals(QueryData.KIND_SODA))? new QueryNodeChildren(queryData) : Children.LEAF, 
        Lookups.fixed( new Object[] { queryData, dataObj } ));
    
    this.queryData = queryData;
    this.setIconBaseWithExtension(IMAGE_ICON_BASE);
    
  }

  /**
   * Gets the dispplay name formatted by html tags.
   * @return display name.
   */
  public String getHtmlDisplayName() {
    String displayname = queryData.getName() + " (<i>" + queryData.getType() + "</i>)"; 
    return displayname;
  }

  /**
   * Returns the preferred action that will be executed on double click.
   * @return preferred node action
   */
  public Action getPreferredAction() {
    Action retValue = this.getActions(true)[0];
    return retValue;
  }
  
  /**
   * Gets the action that could be performed on the node.
   * @param b   flag indicating if the menu is a popup 
   * @return Array of actions to execute on the node
   */
  public Action[] getActions(boolean b) {

    boolean isSoda = queryData.getKind().equals(QueryData.KIND_SODA);
    if( isSoda ) {
      return new Action[] { 
        new ExecuteQueryAction(),
        new DeleteQueryAction(),
        null,
        new AddDescendAction(),
        null,
        SystemAction.get(PropertiesAction.class)      
      };
    } else {
      
      return new Action[] { 
        new ExecuteQueryAction(),
        new DeleteQueryAction(),
        null,
        SystemAction.get(PropertiesAction.class)      
      };
    }
  }
  
  /**
   * Helper to reload the children
   */
  public void reloadChildren() {
    this.setChildren(new QueryNodeChildren(this.queryData));
  }

  /**
   */
  public QueryNode getSource() {
    return this;
  }
  
  /**
   * Retrieves the Db4o provider object holding the database and queries from
   * the lookup object.
   * @return Db4o provider object. 
   */
  private Db4oProvider getDb4oProvider() {

    DataObject dataObj = (DataObject)this.getLookup().lookup(DataObject.class);
    Db4oProvider provider = (Db4oProvider)dataObj.getCookie(Db4oProvider.class);
    if( provider == null ) {
      ErrorManager.getDefault().log("Severe: No db4o provider found");
    }
    return provider;
  }

  
  
  /**
   * Creates the property sheet for this node.
   * @return property sheet
   */
  protected Sheet createSheet() {
    
    // Create property sheet and collection to keep the properties.
    Sheet sheet = Sheet.createDefault();
    Sheet.Set set = sheet.createPropertiesSet();
    try {
      
      // add properties to the sheet
      addPropertiesToSet(set);
        
    } catch (Exception ex) {
        ErrorManager.getDefault().notify (ex);
    }
    sheet.put(set);
    return sheet;
  }

// <editor-fold defaultstate="collapsed" desc=" Properties ">                          
  
  /**
   * Adds node properties to the property sheet.
   * @param set     a set attached to a property sheet taking the properties  
   */
  private void addPropertiesToSet(final Sheet.Set set) throws DOMException {
    
    Property queryNameProp = createNameProperty();
    set.put (queryNameProp);

    Property queryTypeProp = createTypeProperty();
    set.put (queryTypeProp);

    Property queryKindProp = createKindProperty();
    set.put (queryKindProp);
    
    if( this.queryData.getKind().equals(QueryData.KIND_SQL) ) {

      Property querySqlProp = createSqlProperty();
      set.put (querySqlProp);
      
    } else {
      
      Property querySodaProp = createSodaProperty();
      set.put (querySodaProp);
      
    }
  }

  /**
   * Creates property 'name'.
   * @return property object
   */ 
  private Property createNameProperty() throws DOMException {
    
    String displayName = NbBundle.getMessage(this.getClass(), "Name");
    Property queryNameProp = new PropertySupport.ReadWrite("name", String.class, 
            displayName, null) {
      public Object getValue() {
        return QueryNode.this.queryData.getName();
      }
      public void setValue(Object name) {
        QueryNode.this.queryData.setName(name.toString());
        
        Node qlNode = QueryNode.this.getParentNode();
        Node dbNode = qlNode.getParentNode();
        DataObject dataObj = (DataObject)dbNode.getLookup().lookup(DataObject.class);
        dataObj.setModified(true);

        ((QueryListNode)qlNode).reloadChildren();
      } 
    };
    queryNameProp.setName("name");
    return queryNameProp;
  }

  /**
   * Creates property 'query type'.
   * @return property object
   */ 
  private Property createTypeProperty() throws DOMException {

    String displayName = NbBundle.getMessage(this.getClass(), "Type");
    Property queryTypeProp = new PropertySupport.ReadWrite("type", String.class, 
            displayName, null) {
      public Object getValue() {
        return QueryNode.this.queryData.getType();
      }
      public void setValue(Object name) {
        QueryNode.this.queryData.setType(name.toString());
        
        Node qlNode = QueryNode.this.getParentNode();
        Node dbNode = qlNode.getParentNode();
        DataObject dataObj = (DataObject)dbNode.getLookup().lookup(DataObject.class);
        dataObj.setModified(true);

        ((QueryListNode)qlNode).reloadChildren();
      } 
    };
    queryTypeProp.setName("type");
    return queryTypeProp;
  }

  /**
   * Creates property 'query kind' (i.e. SODA or SQL).
   * @return property object
   */ 
  private Property createKindProperty() throws DOMException {
    
   String displayName = NbBundle.getMessage(this.getClass(), "Kind");
   Property queryKindProp = new PropertySupport.ReadOnly("kind", String.class, 
            displayName, null) {
      public Object getValue() {
        return QueryNode.this.queryData.getKind();
      }
      public void setValue(Object name) {
        
        QueryNode.this.queryData.setKind(name.toString());
        
        Node qlNode = QueryNode.this.getParentNode();
        Node dbNode = qlNode.getParentNode();
        DataObject dataObj = (DataObject)dbNode.getLookup().lookup(DataObject.class);
        dataObj.setModified(true);

        ((QueryListNode)qlNode).reloadChildren();
      } 
    };
    queryKindProp.setName("kind");
    return queryKindProp;
  }

  /**
   * Creates a sql specific property for the query expression.
   * @return property object
   */ 
  private Property createSqlProperty() throws DOMException {
    
    String displayName = NbBundle.getMessage(this.getClass(), "Expression");
    Property querySrcProp = new PropertySupport.ReadWrite("expr", String.class, 
            displayName, null) {
      public Object getValue() {
        return QueryNode.this.queryData.getExpression();
      }
      public void setValue(Object name) {
        QueryNode.this.queryData.setExpression( name.toString());
        
        Node qlNode = QueryNode.this.getParentNode();
        Node dbNode = qlNode.getParentNode();
        DataObject dataObj = (DataObject)dbNode.getLookup().lookup(DataObject.class);
        dataObj.setModified(true);

        ((QueryListNode)qlNode).reloadChildren();
      } 
    };
    querySrcProp.setName("expr");
    return querySrcProp;
  }
  
  /**
   * Creates Soda specific property.
   * @return property object
   */ 
  private Property createSodaProperty() {
    
    PropertySupport.Reflection querySrcProp = null;
    try {
      querySrcProp = new PropertySupport.Reflection(QueryNode.this, QueryNode.class, "getSource", null);
      querySrcProp.setName("source");
      querySrcProp.setPropertyEditorClass(SodaPropertyEditor.class);
    } catch(NoSuchMethodException e) {
    }
    return querySrcProp;
  }

// </editor-fold>                          
  

// <editor-fold defaultstate="collapsed" desc=" Children ">                          
  
  public static class QueryNodeChildren  extends Children.Keys {

    private QueryData queryData;

    /** Creates a new instance of Db4oQueryNodeChildren */
    public QueryNodeChildren(QueryData qd) {
      this.queryData = qd;
    }

    protected Node[] createNodes(Object object) {
      DescendNode result = new DescendNode( (Descend) object);
      return new Node[] {result};
    }

    protected void addNotify() {
      
      SodaQueryData sqd = (SodaQueryData) this.queryData;
      if( sqd.getDescend()!=null ) {
        setKeys( new Object[] { sqd.getDescend() } );
      }
    }

  }
    
// </editor-fold>                          
  

// <editor-fold defaultstate="collapsed" desc=" Actions ">                          

  /**
   * Deletes this query node.
   */
  private class DeleteQueryAction extends AbstractAction {
    
    public DeleteQueryAction() {
      String displayName = NbBundle.getMessage(this.getClass(), "DeleteQuery");
      this.putValue(NAME, displayName);
    }
    
    public void actionPerformed(ActionEvent e) {
      
       if (DialogUtil.confirmYesNo("Are you sure to delete this query?")
                     .equals(DialogUtil.YES)) {

          // Remove query from the query list.
          // we ask the parent for its represented data.
          QueryListNode qlNode = (QueryListNode)QueryNode.this.getParentNode();
          qlNode.getQueries().remove( QueryNode.this.queryData );

          // Save it
          Db4oDataNode dataNode = (Db4oDataNode) QueryNode.this.getParentNode().getParentNode();
          DataObject dataObj = dataNode.getDataObject();
          dataObj.setModified(true);

          // Remove from node tree
          QueryNode.this.getParentNode().getChildren().remove(new Node[] {QueryNode.this});
          ((QueryListNode)QueryNode.this.getParentNode()).reloadChildren();

       }
    }
  }
  
  
  /**
   * Executes the query attached to the query node
   */
  private class ExecuteQueryAction extends AbstractAction {
    
    private QueryNode node;
    
    public ExecuteQueryAction() {

      String displayName = NbBundle.getMessage(this.getClass(), "Execute");
      this.putValue(NAME,  displayName);
      
      this.node = QueryNode.this;
    }
    
    public void actionPerformed(ActionEvent e) {

      try {
        
        Db4oDataEditor editor = new Db4oDataEditor();
        if( editor!=null ) {

          Db4oProvider provider = getDb4oProvider();
          if( !provider.getDatabase().isConnected() ) {
            provider.getDatabase().connect();
          }
          ObjectContainer oc = provider.getDatabase().getObjectContainer();

          QueryData queryData = this.node.queryData;
          if( queryData.getKind().equals( QueryData.KIND_SQL ) ) {

            executeSqlQuery(editor, oc, queryData);

          } else if( queryData.getKind().equals( QueryData.KIND_SODA ) ) {

            executeSodaQuery(editor, oc);
          }
        }
        
      } catch ( Db4oProviderException ex ) {
        
        String message = NbBundle.getMessage(this.getClass(), "CannotConnectToDatabase");
        DialogUtil.showErrorMessage(message);
      }
    }

    private void executeSodaQuery(final Db4oDataEditor editor, final ObjectContainer oc) throws HeadlessException {
      
      SodaQueryData queryData = (SodaQueryData)this.node.queryData;
      Query q = this.createQuery(oc, queryData);
      if( q != null) {

        ObjectSet os = null;
 
        // Execute the query
        try {
          os = q.execute();
        } catch(Db4oException e) {
          
          String message = "Error executing the query:\n"+ e.getMessage() + "\n";
          DialogUtil.showErrorMessage(message);
          ErrorManager.getDefault().log(ErrorManager.EXCEPTION, e.getMessage());
        }
        
        // If we have a result, we are gonna display it.
        if( os != null ) {
          int count = os.size();
          
          DataObject dataObj = (DataObject) this.node.getLookup().lookup(DataObject.class);
          
          AbstractNode osNode = new ObjectSetNode(os, oc, dataObj);
          osNode.setDisplayName("Query Results for " + this.node.getDisplayName() + ": #" + count + " records found");

          editor.setDisplayName(editor.getDisplayName() + " - Query Results (#" + count + ")");
          editor.getExplorerManager().setRootContext(osNode);
          editor.open();
          editor.requestActive();
        }

      } else {

        DialogUtil.showErrorMessage(
          "Cannot create query.\nPlease check query definition and " +
            "make sure that requested type is on the classpath");
      }
    }

    private void executeSqlQuery(final Db4oDataEditor editor, final ObjectContainer oc, final QueryData queryData) {

      String query = queryData.getExpression();
      try {

        ObjectSetWrapper osw = (ObjectSetWrapper ) Sql4o.execute(oc, query);
        int count = osw.size();

        DataObject dataObj = (DataObject) this.node.getLookup().lookup(DataObject.class);
        AbstractNode osNode = new ObjectSetNode(osw.ext(), oc, dataObj);
        osNode.setDisplayName("Query Results for " + this.node.getDisplayName() + ": #" + count + " records found");

        editor.setDisplayName(editor.getDisplayName() + " - Query Results (#" + count + ")");
        editor.getExplorerManager().setRootContext(osNode);
        editor.open();
        editor.requestActive();
        
      } catch (SqlParseException ex) {
          DialogUtil.showErrorMessage(
            "Error executing the query:\n"+ ex.getMessage() + "\n");
          ErrorManager.getDefault().log(ErrorManager.EXCEPTION, ex.getMessage());
      } catch (Sql4oException ex) {
          DialogUtil.showErrorMessage(
            "Error executing the query:\n"+ ex.getMessage() + "\n");
          ErrorManager.getDefault().log(ErrorManager.EXCEPTION, ex.getMessage());
      }
    }
    
    private Query createQuery(ObjectContainer oc, SodaQueryData queryData) {
              
      Query q = oc.query();
      
      Object clazz = ClassLoaderUtil.loadClass(queryData.getType(),oc);
      if( clazz != null ) {
        q.constrain(clazz);
        createQueryPart(q, queryData.getDescend() );
        return q;
      }
      return null;
      
    }

    private void createQueryPart(final Query q, final Constrain constrain) {

      // Check precondition
      if( q==null || constrain == null ) {
        return;
      }

      // if it is another descend, build sub query
      if( constrain instanceof Descend ){

        Descend d = (Descend)constrain;
        Query subQuery = q.descend( d.getValue() );

        // Add children to the query
        List children = constrain.getChildren();
        Iterator it = children.iterator();

        while( it.hasNext() ) {

          Object child = it.next();
          createQueryPart(subQuery, (Constrain)child );

        }

      } else if( constrain instanceof Constrain ) {
        // if it is a constrain, add value

        Constrain c = (Constrain)constrain;
        
        Constraint newConstraint = null;
        if( c.getTag().equals( Constants.ATTR_TAG_VALUE ) ) {
          newConstraint = q.constrain( createTypedValue(c.getType(), c.getValue()) );
        }        
        
        if( newConstraint != null ) {
          
           // Add children if any
          List children = constrain.getChildren();
          Iterator it = children.iterator();

          while( it.hasNext() ) {

            Object child = it.next();
            createQueryPart(newConstraint, (Constrain)child );

          }
        }
      }
    }
    
    private void createQueryPart(final Constraint db4oConstraint, final Constrain constrain) {

      // Check precondition
      if( db4oConstraint==null || constrain == null ) {
        return;
      }

      if( constrain instanceof Constrain ) {
      
        Constraint newConstraint = null;
        if( constrain.getTag().equals( Constants.ATTR_TAG_LESS ) ) {
          newConstraint = db4oConstraint.smaller();
        }
        if( constrain.getTag().equals( Constants.ATTR_TAG_GREATER ) ) {
          newConstraint = db4oConstraint.greater();
        }
        if( constrain.getTag().equals( Constants.ATTR_TAG_NOT ) ) {
          newConstraint = db4oConstraint.not();
        }
        
        if( newConstraint != null ) {

          // Add children if any
          List children = constrain.getChildren();
          Iterator it = children.iterator();
          while( it.hasNext() ) {

            Object child = it.next();
            createQueryPart(newConstraint, (Constrain)child );

          }
        }
      }
    }
    
    private Object createTypedValue(String type, String value) {
      
      if( type.equals("int")) {
        return new Integer(value);
      } else if( type.equals("long")) {
        return new Long(value);
      } else if( type.equals("double")) {
        return new Double(value);
      } else if( type.equals("float")) {
        return new Float(value);
      } else {
        try {
          Class clazz = ClassLoaderUtil.getClassLoader().loadClass(type);
          Constructor constr = clazz.getConstructor(new Class[] {String.class});
          return constr.newInstance(new Object[] {value});
          
        } catch (Exception ex) {
          return value;
        }        
      }
    } 
    
  }  
  
   /**
   * Adds a descend node to the query
   */
  private class AddDescendAction extends AbstractAction {

    public AddDescendAction() {
      String displayName = NbBundle.getMessage(this.getClass(), "AddDescend");
      this.putValue(NAME, displayName);
    }

    public void actionPerformed(ActionEvent e) {

      Db4oProvider provider = QueryNode.this.getDb4oProvider();
      ObjectContainer oc = provider.getDatabase().getObjectContainer();

      String type = QueryNode.this.queryData.getType();
      List names = Db4oReflectorUtil.getDeclaredFields(type,oc);

      EnterDescendDialog dlg = new EnterDescendDialog(new javax.swing.JFrame(), true);
      dlg.setNames(names);
      dlg.setVisible(true);
      if( dlg.getReturnStatus() == EnterDescendDialog.RET_CANCEL ) {
        return;
      }

      String name = dlg.getSelectedName();
      if( name == null ) {
        return;
      }

      Descend newDescend = provider.createDescend(type,name);

      ((SodaQueryData)queryData).setDescend(newDescend);
      DataObject dataObj = (DataObject)QueryNode.this.getLookup().lookup(DataObject.class);
      dataObj.setModified(true);

      QueryNode.this.setChildren( new QueryNodeChildren(queryData));
      
    }
  }
  
// </editor-fold>                          
  
}  
